<?php
/*! \brief
 * This is WFFile class
 * This is the Class that manipulate relative File
 * \author William Su
 */
/**
 * 
 * Extra GLOB constant for safe_glob()
 */
define('GLOB_NODIR', 256);
define('GLOB_PATH', 512);
define('GLOB_NODOTS', 1024);
define('GLOB_RECURSE', 2048);
class WFFile
{
    /**
     * @var WF
     */
    private $objFramework;
    private static $instance;
    public function __construct($objFramework){
        $this->objFramework = $objFramework;
    }
    /**
     *\brief Description
     * - This method upload $strPathSource to $strPathDestination
     */
    public function uploadFile($strPathSource, $strPathDestination){
        if(255 < strlen($strPathSource)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strPathSource:[' . $strPathSource . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        if(255 < strlen($strPathDestination)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strPathDestination:[' . $strPathDestination . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        $this->rm($strPathDestination);
        $result = copy($strPathSource, $strPathDestination);
        if(! $result){
            $message = __CLASS__ . '::' . __FUNCTION__ . '()=>Upload Failed From ' . $strPathSource . ' To ' . $strPathDestination;
            throw new WFIOFileException($message);
        }
    }
    /**
     *\brief Description
     * rm() -- Vigorously erase files and directories.
     *
     * @param $fileglob mixed If string, must be a file name (foo.txt), glob pattern (*.txt), or directory name.
     * If array, must be an array of file names, glob patterns, or directories.
     */
    public function rm($fileglob){
        if(is_string($fileglob)){
            if(is_file($fileglob)){
                $ret = unlink($fileglob);
                if(! $ret){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Remove file [' . $fileglob . '] Failed!';
                    throw new WFIOFileException($message);
                }
            }else if(is_dir($fileglob)){
                $ok = $this->rm("$fileglob/*");
                if(! $ok){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Remove file [' . $fileglob . '/*] Failed!';
                    throw new WFIOFileException($message);
                }
                $ret = rmdir($fileglob);
                if(! $ret){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Remove file [' . $fileglob . '] Failed!';
                    throw new WFIOFileException($message);
                }
            }else{
                $matching = glob($fileglob);
                if($matching === false){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>' . sprintf('No files match supplied glob %s', $fileglob);
                    throw new WFIOFileException($message);
                }
                $rcs = array_map(array($this, __FUNCTION__), $matching);
                if(in_array(false, $rcs)){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Remove file [' . ((is_array($matching) && $matching) ? implode(',', $matching) : '') . '] Failed!';
                    throw new WFIOFileException($message);
                }
            }
        }else if(is_array($fileglob)){
            $rcs = array_map(array($this, __FUNCTION__), $fileglob);
            if(in_array(false, $rcs)){
                $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Remove file [' . (implode(',', $fileglob)) . '] Failed!';
                throw new WFIOFileException($message);
            }
        }else{
            $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Param1 must be filename or glob pattern, or array of filenames or glob patterns';
            throw new WFIOFileException($message);
        }
    }
    /**
     *\brief Description
     * - This method rename $strPathOld to $strPathNew
     */
    public function rename($strPathOld, $strPathNew){
        if(255 < strlen($strPathOld)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strPathOld:[' . $strPathOld . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        if(255 < strlen($strPathNew)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strPathNew:[' . $strPathNew . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        $ret = rename($strPathOld, $strPathNew);
        if(! $ret){
            $message = __CLASS__ . '::' . __FUNCTION__ . '()=>Failed to rename from [' . $strPathOld . '] to [' . $strPathOld . ']!';
            throw new WFIOFileException($message);
        }
        return $ret;
    }
    /**
     *\brief Description
     * - This method mkdir $strPath with permission $intMode
     */
    public function mkdir($strPath, $intMode = 0777){
        if(255 < strlen($strPath)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strPath:[' . $strPath . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        $ret = true;
        if(false === file_exists($strPath)){
            $ret = mkdir($strPath, $intMode);
            if(! $ret){
                $message = __CLASS__ . '::' . __FUNCTION__ . '()=>Failed to mkdir [' . $strPath . '] permission [' . $intMode . ']!';
                throw new WFIOFileException($message);
            }
        }
        return $ret;
    }
    /**
     *\brief Description
     * - This method chmod $strPath to permission $intMode
     */
    public function chmod($strPath, $intMode = 0777){
        if(255 < strlen($strPath)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strPath:[' . $strPath . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        $ret = true;
        if(true === file_exists($strPath)){
            $ret = chmod($strPath, $intMode);
            if(! $ret){
                $message = __CLASS__ . '::' . __FUNCTION__ . '()=>Failed to chmod [' . $strPath . '] permission [' . $intMode . ']!';
                throw new WFIOFileException($message);
            }
        }
        return $ret;
    }
    /**
     *\brief Description
     * Copy file or folder from source to destination, it can do 
     * recursive copy as well and is very smart 
     * It recursively creates the dest file or directory path if there weren't exists 
     * Situtaions : 
     * - Src:/home/test/file.txt ,Dst:/home/test/b ,Result:/home/test/b -> If source was file copy file.txt name with b as name to destination 
     * - Src:/home/test/file.txt ,Dst:/home/test/b/ ,Result:/home/test/b/file.txt -> If source was file Creates b directory if does not exsits and copy file.txt into it 
     * - Src:/home/test ,Dst:/home/ ,Result:/home/test/** -> If source was directory copy test directory and all of its content into dest      
     * - Src:/home/test/ ,Dst:/home/ ,Result:/home/**-> if source was direcotry copy its content to dest 
     * - Src:/home/test ,Dst:/home/test2 ,Result:/home/test2/** -> if source was directoy copy it and its content to dest with test2 as name 
     * - Src:/home/test/ ,Dst:/home/test2 ,Result:->/home/test2/** if source was directoy copy it and its content to dest with test2 as name 
     * @todo 
     * - Should have rollback technique so it can undo the copy when it wasn't successful 
     * - Auto destination technique should be possible to turn off 
     * - Supporting callback function 
     * - May prevent some issues on shared enviroments : http://us3.php.net/umask 
     * @param $source file or folder 
     * @param $dest file or folder 
     * @param $options folderPermission,filePermission 
     * @return boolean if copy success return true, if fail will raised an Exception
     */
    public function copyRecurse($source, $dest, $options = array('folderPermission'=>0755,'filePermission'=>0755)){
        if(255 < strlen($source)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$source:[' . $source . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        if(255 < strlen($dest)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$dest:[' . $dest . '] Path length too long and exceed 255 limit! Please input a valid path!';
            throw new WFIOFileException($strMsg);
        }
        $retResult = false;
        if(is_file($source)){
            if($dest[strlen($dest) - 1] == '/'){
                if(! file_exists($dest)){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>File or Folder Destination doesn\'t Existed! [' . $dest . ']!';
                    throw new WFIOFileException($message);
                }
                $__dest = $dest . basename($source);
            }else{
                $__dest = $dest;
            }
            $retResult = copy($source, $__dest);
            if(! $retResult){
                $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>Failed copy file from [' . $source . '] to [' . $__dest . ']!';
                throw new WFIOFileException($message);
            }
            $resultChmod = $this->chmod($__dest, $options['filePermission']);
        }elseif(is_dir($source)){
            if($dest[strlen($dest) - 1] == '/'){
                if($source[strlen($source) - 1] == '/'){
                    //Copy only contents 
                }else{
                    //Change parent itself and its contents 
                    $dest = $dest . basename($source);
                    ////@mkdir ( $dest );
                    $resultMkdir = $this->mkdir($dest);
                    $resultChmod = $this->chmod($dest, $options['filePermission']);
                }
            }else{
                $resultMkdir = $this->mkdir($dest, $options['folderPermission']);
                $resultChmod = $this->chmod($dest, $options['filePermission']);
            }
            $dirHandle = opendir($source);
            while($file = readdir($dirHandle)){
                if($file != "." && $file != ".."){
                    $__dest = $dest . "/" . $file;
                    $retResult = $this->copyRecurse($source . "/" . $file, $__dest, $options);
                }
            }
            closedir($dirHandle);
        }else{
            ////$retResult = false;
            $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>[' . $source . '] is not a valid file!';
            throw new WFIOFileException($message);
        }
        return $retResult;
    }
    /**
     *\brief Description
     * - This method will check all of file is writable or not
     * - If one of the file or folder(include root folder) is not writable, 
     * - will throw an exception, else return true
     */
    public function isWritableRecursive($strDirPath){
        if(false === is_dir($strDirPath)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strDirPath is not a Dir :[' . $strDirPath . ']';
            throw new WFIOFileException($strMsg);
        }
        if(false === $this->isWritable($strDirPath)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>Dir is not writable :[' . $strDirPath . ']';
            throw new WFIOFileException($strMsg);
        }
        $arrFilePath = $this->getArrPathByFolderPath($strDirPath);
        foreach($arrFilePath as $file){
            $strPathFile = $file;
            $is_writeable = $this->isWritable($strPathFile);
            if(false === $is_writeable){
                $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>File [' . $strPathFile . '] in [' . $strDirPath . '] is not writable :[' . $file . ']';
                throw new WFIOFileException($strMsg);
            }
        }
        return true;
    }
    /**
     *\brief Description
     * - This method will check whether the file or dir is writable, 
     * - if is writable return true, else return false
     * will work in despite of Windows ACLs bug
     * NOTE: use a trailing slash for folders!!!
     * see http://bugs.php.net/bug.php?id=27609
     * see http://bugs.php.net/bug.php?id=30931
     */
    public function isWritable($path){
        if($path{strlen($path) - 1} == '/'){ //if $path=folder, return $path/unique.tmp
            return $this->isWritable($path . uniqid(mt_rand()) . '.tmp');
        }else if(is_dir($path)){ //if $path=folder, return $path/unique.tmp
            return $this->isWritable($path . '/' . uniqid(mt_rand()) . '.tmp');
        }
        // $path/unique.tmp is not exists before fopen
        $isUniqidTmpFile = ! file_exists($path);
        // check tmp file for read/write capabilities
        $f = @fopen($path, 'a');
        if($f === false){
            return false;
        }
        fclose($f);
        if(true === $isUniqidTmpFile){
            unlink($path);
        }
        return true;
    }
    public function isReadable($path){
        return is_readable($path) ? true : false;
    }
    /**
     *\brief Description
     * - This method will check all of file is readable and writable or not
     * - If one of the file or folder(include root folder) is not readable or not writable, 
     * - will throw an exception, else return true
     */
    public function isReadableWritableRecursive($strDirPath){
        $this->isReadableRecursive($strDirPath);
        $this->isWritableRecursive($strDirPath);
        return true;
    }
    /**
     *\brief Description
     * - This method will check all of file is readable and writable or not
     * - If one of the file or folder(include root folder) is not readable or not writable, 
     * - will throw an exception, else return true
     */
    public function isReadableWritable($strPath){
        $isReadable = $this->isReadable($strPath);
        $isWritable = $this->isWritable($strPath);
        return ($isReadable && $isWritable);
    }
    /**
     *\brief Description
     * - This method will check all of file is readable or not
     * - If one of the file or folder(include root folder) is not readable, 
     * - will throw an exception, else return true
     */
    public function isReadableRecursive($strDirPath){
        if(false === is_dir($strDirPath)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>$strDirPath is not a Dir :[' . $strDirPath . ']';
            throw new WFIOFileException($strMsg);
        }
        if(false === is_readable($strDirPath)){
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>Dir is not readable :[' . $strDirPath . ']';
            throw new WFIOFileException($strMsg);
        }
        $arrFilePath = $this->getArrPathByFolderPath($strDirPath);
        foreach($arrFilePath as $file){
            $is_readable = is_readable($file);
            if(false === $is_readable){
                $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>File [' . $file . '] in [' . $strDirPath . '] is not readable :[' . $file . ']';
                throw new WFIOFileException($strMsg);
            }
        }
        return true;
    }
    /**
     *\brief Description
     * - Just tell it what directory you want deleted, in relation to the page that this function is executed. 
     * - Then set $empty = true if you want the folder just emptied, but not deleted. 
     * - If you set $empty = false, or just simply leave it out, the given directory will be deleted, as well.
     */
    public function rmdirRecurse($directory, $empty = false){
        if(substr($directory, - 1) == "/"){
            $directory = substr($directory, 0, - 1);
        }
        if(! file_exists($directory) || ! is_dir($directory)){
            /*$message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>[' . $directory . '] is not exists or is not folder!';
            throw new WFIOFileException($message);*/
            return;
        }
        
        if(! is_readable($directory)){
            $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>[' . $directory . '] is not readable!';
            throw new WFIOFileException($message);
        }else{
            $directoryHandle = opendir($directory);
            while($contents = readdir($directoryHandle)){
                if($contents != '.' && $contents != '..'){
                    $path = $directory . "/" . $contents;
                    if(is_dir($path)){
                        $this->rmdirRecurse($path);
                    }else{
                        $ret = $this->rm($path);
                        if(false === $ret){
                            closedir($directoryHandle);
                            $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>failed to remove file [' . $path . ']!';
                            throw new WFIOFileException($message);
                        }
                    }
                }
            }
            closedir($directoryHandle);
            if($empty == false){
                if(! rmdir($directory)){
                    $message = __CLASS__ . '::' . __FUNCTION__ . '()' . __LINE__ . '=>failed to rmdir [' . $directory . ']!';
                    throw new WFIOFileException($message);
                }
            }
            return true;
        }
    }
    /**
     *\brief Description
     * - This method will get the longest file path from specify dir path
     */
    public function getLongestFilePathInDir($strDirPath){
        $ret = $this->getLongestFilePathAndLengthInDir($strDirPath, 'file_path');
        return $ret;
    }
    /**
     *\brief Description
     * - This method will get the longest length of file path from specify dir path
     */
    public function getLongestFileLengthInDir($strDirPath){
        $ret = $this->getLongestFilePathAndLengthInDir($strDirPath, 'file_length');
        return $ret;
    }
    private function getLongestFilePathAndLengthInDir($strDirPath, $strFilePathOrFileLength){
        $key = md5($strDirPath);
        if(! isset(self::$instance['getLongestFilePathAndLengthInDir'][$key])){
            $dir_iterator = new RecursiveDirectoryIterator($strDirPath);
            $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
            $strLongestFilePath = '';
            $intLongestFilePathLength = 0;
            foreach($iterator as $file){
                $intFileLength = strlen($file->getPathName());
                if($intFileLength > $intLongestFilePathLength){
                    $intLongestFilePathLength = $intFileLength;
                    $strLongestFilePath = $file->getPathName();
                }
            }
            self::$instance['getLongestFilePathAndLengthInDir'][$key]['file_length'] = $intLongestFilePathLength;
            self::$instance['getLongestFilePathAndLengthInDir'][$key]['file_path'] = $strLongestFilePath;
        }
        return self::$instance['getLongestFilePathAndLengthInDir'][$key][$strFilePathOrFileLength];
    }
    /**
     *\brief Description
     * - This method write data to file safety using flock
     * - And retry mechanism
     */
    public function writeDataSafe($path, $mode, $data){
        $fp = fopen($path, $mode);
        $retries = 0;
        $max_retries = 100;
        if(! $fp){
            // failure 
            ////return false;
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>Failed to fopen path:[' . $path . '] mode:[' . $mode . ']';
            throw new WFIOFileException($strMsg);
        }
        // keep trying to get a lock as long as possible 
        do{
            if($retries > 0){
                usleep(rand(1, 10000));
            }
            $retries += 1;
        }while(! flock($fp, LOCK_EX) and $retries < $max_retries);
        // couldn't get the lock, give up 
        if($retries == $max_retries){
            // failure 
            ////return false;
            $strMsg = __CLASS__ . '::' . __FUNCTION__ . '()=>Failed to get the flock with path:[' . $path . '] retry:[' . $max_retries . ']';
            throw new WFIOFileException($strMsg);
        }
        // got the lock, write the data 
        fwrite($fp, $data);
        // release the lock 
        flock($fp, LOCK_UN);
        fclose($fp);
        // success 
        return true;
    }
    /**
     *\brief Description
     * - This method get last line from file
     */
    public function getFileContentsLastLine($strPathFileName){
        $line = '';
        $f = fopen($strPathFileName, 'r');
        $cursor = - 1;
        fseek($f, $cursor, SEEK_END);
        $char = fgetc($f);
        /**
         * Trim trailing newline chars of the file
         */
        while($char === "\n" || $char === "\r"){
            fseek($f, $cursor--, SEEK_END);
            $char = fgetc($f);
        }
        /**
         * Read until the start of file or first newline char
         */
        while($char !== false && $char !== "\n" && $char !== "\r"){
            /**
             * Prepend the new char
             */
            $line = $char . $line;
            fseek($f, $cursor--, SEEK_END);
            $char = fgetc($f);
        }
        return $line;
    }
    /**
     *\brief Description
     * - This method can set XML header
     * - return void
     *\par Variable
     * - null
     */
    public function setXMLHeader(){
        header('Content-Type: text/xml');
    }
    /**
     *\brief Description
     * - This method will return array path of file and folder by input a folder path
     * EX : $strFolderPath = '/Users/jersus/htdocs/CruiseControl/projects/Widget_Framework/TMShare/AJAX-Widget/Dev/AJAX-Widgets-1.7/AJAX-Widgets/src/inc/class/../../repository/widgets_new'
     */
    public function getArrPathByFolderPath($strFolderPath){
        $files = array();
        $arrPattern = array();
        $arrPattern[] = GLOB_MARK | GLOB_NOSORT;
        $arrPattern[] = GLOB_ONLYDIR;
        foreach($arrPattern as $constPattern){
            $file_tmp = glob($strFolderPath . '*', $constPattern);
            if(is_array($file_tmp) && $file_tmp){
                foreach($file_tmp as $item){
                    if(substr($item, - 1) != DIRECTORY_SEPARATOR){
                        $files[] = $item;
                    }else{
                        $files = array_merge($files, $this->getArrPathByFolderPath($item));
                    }
                }
            }
        }
        return $files;
    }
    /**
     *\brief Description
     * - Check is file exists or not. If exists return true else return false
     */
    public function isFileExists($strPath){
        $key = base64_encode($strPath);
        if(!isset(self::$instance[__FUNCTION__][$key])){
            self::$instance[__FUNCTION__][$key] = file_exists($strPath);
        }
        return self::$instance[__FUNCTION__][$key];
    }
}